/**
 * Code template comes from Eclipse plug-in "Bytecode"
 */
package com.googlecode.cswish.struts.spring.generator;

import static com.googlecode.cswish.asm.Opcodes.ACC_PUBLIC;
import static com.googlecode.cswish.asm.Opcodes.ACONST_NULL;
import static com.googlecode.cswish.asm.Opcodes.ALOAD;
import static com.googlecode.cswish.asm.Opcodes.ARETURN;
import static com.googlecode.cswish.asm.Opcodes.ASTORE;
import static com.googlecode.cswish.asm.Opcodes.CHECKCAST;
import static com.googlecode.cswish.asm.Opcodes.DUP;
import static com.googlecode.cswish.asm.Opcodes.GETSTATIC;
import static com.googlecode.cswish.asm.Opcodes.GOTO;
import static com.googlecode.cswish.asm.Opcodes.ICONST_0;
import static com.googlecode.cswish.asm.Opcodes.ICONST_1;
import static com.googlecode.cswish.asm.Opcodes.IFEQ;
import static com.googlecode.cswish.asm.Opcodes.IFNE;
import static com.googlecode.cswish.asm.Opcodes.INVOKESPECIAL;
import static com.googlecode.cswish.asm.Opcodes.INVOKESTATIC;
import static com.googlecode.cswish.asm.Opcodes.INVOKEVIRTUAL;
import static com.googlecode.cswish.asm.Opcodes.IRETURN;
import static com.googlecode.cswish.asm.Opcodes.NEW;

import java.util.List;

import org.apache.commons.lang.StringUtils;

import com.googlecode.cswish.asm.ClassWriter;
import com.googlecode.cswish.asm.Label;
import com.googlecode.cswish.asm.MethodVisitor;
import com.googlecode.cswish.asm.Type;
import com.googlecode.cswish.struts.spring.NameType;

/**
 * Use the Map interface to wrap the Value object
 * @author Jerry.feng Date: 10/09/2008
 */
public class MapWrapHelper {

	public static void defineMap(String classDefName, List<NameType> properties, ClassWriter cw) {
		String classDefNameDesc = "L" + classDefName + ";";
		defineMapGet(classDefName, classDefNameDesc, properties, cw);
		defineMapPut(classDefName, classDefNameDesc, properties, cw);
		defineContainsKey(classDefName, classDefNameDesc, properties, cw);
		// TODO: is it necessary to process the other methods?
		// size(), values(), containsKey() etc
	}

	private static void defineContainsKey(String classDefName,
			String classDefNameDesc, List<NameType> properties, ClassWriter cw) {
		// public boolean containsKey(Object key) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "containsKey", "(Ljava/lang/Object;)Z", null, null);
		mv.visitCode();
		
		Label begin = new Label();
		mv.visitLabel(begin);
		Label found = new Label();
		Label end = new Label();
		
		// "f1".equals(key)
		if (properties != null && properties.size() > 0) {
			for (NameType field : properties) {
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitLdcInsn(field.name);
				mv.visitVarInsn(ALOAD, 1);
				mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z");
				mv.visitJumpInsn(IFNE, found);
			}
		}
		
		// return true
		mv.visitLabel(found);
		mv.visitInsn(ICONST_1);
		mv.visitInsn(IRETURN);
		
		// return false;
		mv.visitLabel(end);
		mv.visitInsn(ICONST_0);
		mv.visitInsn(IRETURN);
		
		Label l5 = new Label();
		mv.visitLabel(l5);
		mv.visitLocalVariable("this", classDefNameDesc, null, begin, l5, 0);
		mv.visitLocalVariable("key", "Ljava/lang/Object;", null, begin, l5, 1);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	/*
	// 1
	public Object put(Object key, Object value) {
		// 2
		String sKey = (String) key;
		// 3
		String methodName = "set" + 
			Character.toUpperCase(sKey.charAt(0)) + sKey.substring(1);
		// 4
		Object result = null;
		// 5
		Object convertedValue = null;
		// 6
		if ("setF1".equals(methodName)) {
			// 7
			System.out.println(methodName);
			
			// 8
			convertedValue = GeneratorHelper.convert(value, String.class);
			convertedValue = GeneratorHelper.convert(value, boolean.class);
			
			// 9
			setF1((String) convertedValue);
			
			// 10
			System.out.println("end");
			
			convertedValue = (Boolean) convertedValue;
			setF2((Boolean) convertedValue);
		}
		
		// 11
		return null;
	}
	*/
	private static void defineMapPut(String classDefName,
			String classDefNameDesc, List<NameType> properties, ClassWriter cw) {
		// 1. public Object put(Object key, Object value) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", null, null);
		mv.visitCode();
		
		// 2. String sKey = (String) key;
		Label l0 = new Label();
		mv.visitLabel(l0);
		mv.visitVarInsn(ALOAD, 1);	// var1 (method parameter)
		mv.visitTypeInsn(CHECKCAST, "java/lang/String");
		mv.visitVarInsn(ASTORE, 3);	// var3 (local variable)
		
		// 3. String methodName = "set" + Character.toUpperCase(sKey.charAt(0)) + sKey.substring(1);
		Label l1 = new Label();
		mv.visitLabel(l1);
		mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
		mv.visitInsn(DUP);
		mv.visitLdcInsn("set");
		mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");
		Label l2 = new Label();
		mv.visitLabel(l2);
		mv.visitVarInsn(ALOAD, 3);
		mv.visitInsn(ICONST_0);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "charAt", "(I)C");
		mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "toUpperCase", "(C)C");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(C)Ljava/lang/StringBuilder;");
		mv.visitVarInsn(ALOAD, 3);
		mv.visitInsn(ICONST_1);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "substring", "(I)Ljava/lang/String;");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
		Label l3 = new Label();
		mv.visitLabel(l3);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
		mv.visitVarInsn(ASTORE, 4);	// var4 (methodName)
		
		// 4. Object result = null;
		Label l4 = new Label();
		mv.visitLabel(l4);
		mv.visitInsn(ACONST_NULL);
		mv.visitVarInsn(ASTORE, 5);
		// 5. Object convertedValue = null;
		mv.visitInsn(ACONST_NULL);
		mv.visitVarInsn(ASTORE, 6);
		
		Label begin = new Label();
		mv.visitLabel(begin);
		Label nextCondition = null;
		Label end = new Label();
		// Call "set" method for each field
		if (properties != null && properties.size() > 0) {
			for (NameType field : properties) {
				if (field.disableSetter) {
					continue;
				}
				
				String capName = StringUtils.capitalize(field.name);
				String fieldDefName = field.type.replace('.', '/');
				String fieldNameDesc = Type.getDescriptor(field.type);
				String setMethodName = "set" + capName;
				String setMethodDesc = "(" + fieldNameDesc + ")V";
				
				// 6. if ("setF1".equals(methodName)) {
				if (nextCondition != null) {
					mv.visitLabel(nextCondition);
				}
				mv.visitLdcInsn(setMethodName);
				mv.visitVarInsn(ALOAD, 4);
				mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z");
				nextCondition = new Label();
				mv.visitJumpInsn(IFEQ, nextCondition);
				
				//String field = (String) OgnlOps.convertValue(value, String.class);
				//setF1(field);
				Label callFunction = new Label();
				mv.visitLabel(callFunction);
				
				// 7. test code
//				mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//				mv.visitVarInsn(ALOAD, 4);
//				mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
				
				// 8. convertedValue = GeneratorHelper.convert(value, String.class);
				mv.visitVarInsn(ALOAD, 2);
				if (field.isPrimitive()) {
					String matchedDesc = field.type.replace('.', '/');
					mv.visitFieldInsn(GETSTATIC, matchedDesc, "TYPE", "Ljava/lang/Class;");
				} else {
					mv.visitLdcInsn(Type.getTypeByDesc(fieldNameDesc));
				}
				String convertUtilName = GeneratorHelper.class.getName().replace('.', '/');
				mv.visitMethodInsn(INVOKESTATIC, convertUtilName,
						"convert", "(Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;");
				mv.visitVarInsn(ASTORE, 6);
				
				// 9. setF1(convertedValue)
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ALOAD, 6);
				if (field.isPrimitive()) {
					String matchedDesc = field.type.replace('.', '/');
					mv.visitTypeInsn(CHECKCAST, matchedDesc);
					MethodVisitorHelper.convertToPrivimitive(mv, field.type);
				} else {
					mv.visitTypeInsn(CHECKCAST, fieldDefName);
				}
				mv.visitMethodInsn(INVOKEVIRTUAL, classDefName, setMethodName, setMethodDesc);
				
				// 10. test code
//				mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//				mv.visitLdcInsn("return");
//				mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
				
				mv.visitJumpInsn(GOTO, end);
			}
		}
		if (nextCondition != null) {
			mv.visitLabel(nextCondition);
		}
		mv.visitLabel(end);
		
		// 11. return null;
		mv.visitInsn(ACONST_NULL);
		mv.visitInsn(ARETURN);
		
		Label l8 = new Label();
		mv.visitLabel(l8);
		mv.visitLocalVariable("this", classDefNameDesc, null, l0, l8, 0);
		mv.visitLocalVariable("key", "Ljava/lang/Object;", null, l0, l8, 1);
		mv.visitLocalVariable("value", "Ljava/lang/Object;", null, l0, l8, 2);
		mv.visitLocalVariable("sKey", "Ljava/lang/String;", null, l1, l8, 3);
		mv.visitLocalVariable("methodName", "Ljava/lang/String;", null, l4, l8, 4);
		mv.visitLocalVariable("result", "Ljava/lang/Object;", null, begin, l8, 5);
		mv.visitLocalVariable("convertedValue", "Ljava/lang/Object;", null, begin, l8, 6);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	private static void defineMapGet(String classDefName,
			String classDefNameDesc, List<NameType> properties, ClassWriter cw) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", null, null);
		mv.visitCode();
		
		// String sKey = (String) key;
		Label l0 = new Label();
		mv.visitLabel(l0);
		mv.visitVarInsn(ALOAD, 1);	// var1
		mv.visitTypeInsn(CHECKCAST, "java/lang/String");
		mv.visitVarInsn(ASTORE, 2);	// var2
		
		// String methodName = "get" + Character.toUpperCase(sKey.charAt(0)) + sKey.substring(1);
		Label l1 = new Label();
		mv.visitLabel(l1);
		mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
		mv.visitInsn(DUP);
		mv.visitLdcInsn("get");
		mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");
		Label l2 = new Label();
		mv.visitLabel(l2);
		mv.visitVarInsn(ALOAD, 2);
		mv.visitInsn(ICONST_0);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "charAt", "(I)C");
		mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "toUpperCase", "(C)C");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(C)Ljava/lang/StringBuilder;");
		mv.visitVarInsn(ALOAD, 2);
		mv.visitInsn(ICONST_1);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "substring", "(I)Ljava/lang/String;");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
		Label l3 = new Label();
		mv.visitLabel(l3);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
		mv.visitVarInsn(ASTORE, 3);	// var3
		
		// Object result = null;
		Label l4 = new Label();
		mv.visitLabel(l4);
		mv.visitInsn(ACONST_NULL);
		mv.visitVarInsn(ASTORE, 4);	// var4
		
		Label begin = new Label();
		mv.visitLabel(begin);
		Label nextCondition = null;
		Label end = new Label();
		// for each field
		if (properties != null && properties.size() > 0) {
			for (NameType field : properties) {
				String capName = StringUtils.capitalize(field.name);
				String fieldNameDesc = Type.getDescriptor(field.type);
				// FIXME: Maybe, the method is 'isXXX' when the property type is boolean
				String getMethodName = "get" + capName;
				String getMethodDesc = "()" + fieldNameDesc;
				
				// if ("getF1".equals(methodName)) {
				if (nextCondition != null) {
					mv.visitLabel(nextCondition);
				}
				mv.visitLdcInsn(getMethodName);
				mv.visitVarInsn(ALOAD, 3);
				mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z");
				nextCondition = new Label();
				mv.visitJumpInsn(IFEQ, nextCondition);
				
				// result = getF1();
				Label callFunction = new Label();
				mv.visitLabel(callFunction);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitMethodInsn(INVOKEVIRTUAL, classDefName, getMethodName, getMethodDesc);
				if (field.isPrimitive()) {
					MethodVisitorHelper.convertToObject(mv, field.type);
				}
				mv.visitVarInsn(ASTORE, 4);

				mv.visitJumpInsn(GOTO, end);
			}
		}
		if (nextCondition != null) {
			mv.visitLabel(nextCondition);
		}
		mv.visitLabel(end);
		
		// return result
		//mv.visitFrame(F_SAME, 0, null, 0, null);
		mv.visitVarInsn(ALOAD, 4);
		mv.visitInsn(ARETURN);
		
		Label l8 = new Label();
		mv.visitLabel(l8);
		mv.visitLocalVariable("this", classDefNameDesc, null, l0, l8, 0);
		mv.visitLocalVariable("key", "Ljava/lang/Object;", null, l0, l8, 1);
		mv.visitLocalVariable("sKey", "Ljava/lang/String;", null, l1, l8, 2);
		mv.visitLocalVariable("methodName", "Ljava/lang/String;", null, l4, l8, 3);
		mv.visitLocalVariable("result", "Ljava/lang/Object;", null, begin, l8, 4);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
}
